Here are some sample plots using the voting data. Any thoughts about what this tells us for analysis?
Setup
Note the setup variables below. These are supposed to be controls all the plots. Sometimes they are. Sometimes not. Have to clean this up, but the goal is uniformity across all the plots.
For fonts, I tried to use ‘sans’ which should pick the system sans serif font for whatever OS is running. In the plotly plot I had to pick a specific font or it defaults to Times New Roman. I picked Arial, but we should look at including Helvetica for macOS users.
# --- Load libraries ---library(ggplot2)library(ggforce)library(dplyr)library(knitr)library(readr)library(sf)library(tigris)library(plotly)# --- Global color palette (Civic Triangle style) ---fill_col <-"#ffffff"# white backgroundline_col <-"#3a5f7d"# blue-grey for roads and outlinestext_col <-"#2f3b44"# text and titlesalt_line <-"#536cae"# secondary line coloralt_text <-"#536cae"# secondary text colorborder_col <-"#e6eef5"# light blue-grey for county borders# --- Load Texas county-level data ---turnout <-read_csv("tx_county_voting_data_2020.csv")# --- Compute voter turnout rate ---turnout <- turnout %>%mutate(Turnout_Rate = (Total_Votes_Cast / Registered_Voters) *100 )# --- Map education categories to years of schooling ---years_map <-c(AMRZE002 =0, AMRZE003 =0, AMRZE004 =0,AMRZE005 =1, AMRZE006 =2, AMRZE007 =3, AMRZE008 =4,AMRZE009 =5, AMRZE010 =6, AMRZE011 =7, AMRZE012 =8,AMRZE013 =9, AMRZE014 =10, AMRZE015 =11, AMRZE016 =12,AMRZE017 =12, AMRZE018 =12, AMRZE019 =13, AMRZE020 =14,AMRZE021 =14, AMRZE022 =16, AMRZE023 =18,AMRZE024 =19, AMRZE025 =20)edu_cols <-intersect(names(turnout), names(years_map))# --- Compute mean years of schooling and normalized education index (0–1) ---turnout <- turnout %>%mutate(total_edu_pop =rowSums(across(all_of(edu_cols)), na.rm =TRUE),mean_years =rowSums(across(all_of(edu_cols)) *unname(years_map[edu_cols]), na.rm =TRUE) / total_edu_pop,edu_index = (mean_years -0) / (20-0) )# --- Create composite Civic variable combining Voting and Education ---turnout <- turnout %>%mutate(# Composite civic vitality scorevote_edu = (Turnout_Rate/100+ edu_index) /2 )# --- Preview the first few rows to confirm ---head(turnout)
library(ggplot2)library(ggforce)# --- Triangle node coordinates ---triangle <-data.frame(label =c("Education", "Health", "Voter Turnout"),x =c(0, 1, 0.5),y =c(0, 0, sqrt(3)/2))# --- Arrows for feedback loops ---arrows <-data.frame(x =c(0.5, 1, 0),y =c(sqrt(3)/2, 0, 0),xend =c(1, 0, 0.5),yend =c(0, 0, sqrt(3)/2))# --- Plot ---p <-ggplot() +# 1. Draw curved arrows FIRST (behind nodes)geom_curve(data = arrows, aes(x = x, y = y, xend = xend, yend = yend),curvature =-0.25,arrow =arrow(length =unit(0.3, "cm")),color = line_col, linewidth =0.9) +# 2. Triangle outlinegeom_polygon(data = triangle, aes(x, y),fill = fill_col, color = line_col, linewidth =1.2) +# 3. Draw nodes ON TOP of curves to hide gapsgeom_point(data = triangle, aes(x, y),size =8, stroke =1.5, shape =21,fill = line_col, color = line_col) +# 4. Labels with spacing adjustmentsgeom_text(data = triangle, aes(x, y, label = label),vjust =c(2.3, 2.3, -3.6),size =5, family ="sans",fontface ="bold", color = text_col) +# 5. Central annotation (lowered)annotate("text", x =0.5, y =0.32,label ="Mutual Reinforcement\nand Feedback",color = text_col, size =4.2,family ="sans", lineheight =1.2) +theme_void() +coord_equal(xlim =c(-0.25, 1.25), ylim =c(-0.25, 1.05), clip ="off") +theme(plot.margin =margin(50, 50, 50, 50),plot.title =element_text(family ="sans", face ="bold", size =16,hjust =0.5, color = text_col ) ) +ggtitle("The Civic Triangle: Education, Health, and Voter Turnout")# --- Display inline ---p# --- Export PNG with full bounding box ---ggsave("civic_triangle_final.png", plot = p, width =9, height =8,dpi =300, units ="in", limitsize =FALSE)
Figure 1: The Civic Triangle linking Education, Health, and Voter Turnout as mutually reinforcing dimensions of civic vitality.
Figure 2
This chart shows name and turn out on hover.
The interesting thing to note is that the exurbs have higher turnout than urban cores, but the most rural areas have the lowest turnout. Might need to do some math on this based on classifying counties as rural, urban, and suburban to see what shakes out.
# --- Load Texas county geometries ---options(tigris_use_cache =TRUE)tx_counties <-counties(state ="TX", cb =TRUE, class ="sf") %>%mutate(County =gsub(" County", "", NAME))# --- Merge turnout and compute quintiles ---tx_map <- tx_counties %>%left_join(turnout, by ="County") %>%mutate(quintile =ntile(-Turnout_Rate, 5))# --- Load and simplify major roads ---roads <-primary_roads(class ="sf")roads_tx <-st_intersection(roads, st_union(st_geometry(tx_counties))) %>%st_cast("LINESTRING") %>%st_coordinates() %>%as.data.frame() %>%rename(lon = X, lat = Y)# --- Quintile fill palette (yellow → red) ---palette_turnout <-rev(c("#8b0000", "#d73a1f", "#f97c18", "#ffb94e", "#fff5a1"))# --- Build ggplot ---p_map <-ggplot() +geom_sf(data = tx_map,aes(fill =factor(quintile),text =paste0("<b>", County, " County</b><br>","Turnout Rate: ", round(Turnout_Rate, 1), "%" ) ),color = border_col, linewidth =0.25 ) +geom_path(data = roads_tx,aes(x = lon, y = lat, group = L1),color =adjustcolor(line_col, alpha.f =0.4),linewidth =0.4 ) +scale_fill_manual(values = palette_turnout,breaks =c("1", "2", "3", "4", "5"),labels =c("Highest Turnout", "", "", "", "Lowest Turnout"),name =NULL,drop =FALSE ) +coord_sf() +theme_void() +theme(plot.background =element_rect(fill = fill_col, color =NA),panel.background =element_rect(fill = fill_col, color =NA),legend.background =element_rect(fill = fill_col, color =NA),legend.position ="right",legend.direction ="vertical",legend.justification ="center",legend.key.width =unit(0.5, "cm"),legend.key.height =unit(0.8, "cm"),legend.text =element_text(family ="Arial", color = text_col, size =10),plot.title =element_text(family ="Arial", face ="bold", size =16,hjust =0.5, color = text_col ),plot.margin =margin(40, 40, 40, 40) ) +ggtitle("Texas County Voter Turnout, 2020")# --- Convert to interactive Plotly map ---p_map_interactive <-ggplotly(p_map, tooltip ="text") %>%layout(font =list(family ="Arial, Helvetica, sans-serif", color = text_col),paper_bgcolor = fill_col,plot_bgcolor = fill_col,title =list(text ="<b>Texas County Voter Turnout, 2020</b>",font =list(family ="Arial, Helvetica, sans-serif", size =18, color = text_col),x =0.5, xanchor ="center" ),legend =list(orientation ="v",x =1.02,y =0.5,xanchor ="left",yanchor ="middle",traceorder ="normal",font =list(family ="Arial, Helvetica, sans-serif", color = text_col, size =11) ) )# --- Fix legend labels in Plotly output (keep all colors visible) ---for (i inseq_along(p_map_interactive$x$data)) {if (!is.null(p_map_interactive$x$data[[i]]$name)) { p_map_interactive$x$data[[i]]$name <-switch( p_map_interactive$x$data[[i]]$name,"1"="Highest Turnout","5"="Lowest Turnout","2"=" ","3"=" ","4"=" ", p_map_interactive$x$data[[i]]$name ) }}p_map_interactive